#! /usr/bin/perl -w
# -------------------------------------------------------------
# ExMon
# written by Brandon Woodson
#
# version 0.9 - May 17th 2007
# -------------------------------------------------------------

use strict;
use DBI;
use Net::Ping;
use Net::SNMP;
use Nmap::Scanner;
use IO::File;
use POSIX qw(WNOHANG setsid);

use constant PID_FILE => '/var/run/exmon.pid';
my $nmap_path = '/usr/bin/nmap';
my $quit = 0;

## network config
my $iprange = "192.168.2.";
my $start_ip = "79";
my $end_ip = "79";

## setup the OID's
my $sysUpTimeOID = '.1.3.6.1.2.1.1.3.0';					# uptime
my $operatingSystemOID = '.1.3.6.1.2.1.1.1.0';				# operating system
my $hostOID = '.1.3.6.1.2.1.1.5.0';							# host name
my $installedApplicationsOID = '.1.3.6.1.2.1.25.6.3.1.2';	# installed software

## database config
my $db = "ExMon";
my $db_host = "localhost";
my $db_port = "3306"; #"8889";										# default for MySQL = 3306
my $db_user = "root";
my $db_pass = "";

## other global variables
my $ip_address;
my $mac_address;
my $operating_system;
my $hardware;
my $host_name;
my $user;
my $uptime;
my @installedApplications;
my $pid = 0;

## signal handler for child die events
$SIG{CHLD} = sub {while (waitpid(-1,WNOHANG) > 0) {}};
$SIG{TERM} = $SIG{INT} = sub { $quit++ };

####################################################
#
# Main Application
#
####################################################

my $fh = open_pid_file(PID_FILE);
$pid = become_daemon();
print $fh $pid;
close $fh;

print "\n0\n";

while(!$quit) {
	for (my $i = $start_ip; $i <= $end_ip; $i++) {
		if(!$quit) {
			my $temp_ip = join("", ($iprange, $i));
			my $scannable = 0;
			$scannable = check_db_for_scannable($temp_ip);
			if( $scannable == 1) {
				ping_ip($temp_ip);
				snmp_ip($temp_ip);
				nmap_ip($temp_ip);
				sleep(1);	
			}
		} else {
			$i = $end_ip + 1;
		}
	}
}


####################################################
#
# Subroutines
#
####################################################

sub check_db_for_scannable {
	my $ip = shift(@_);
	
	my $dbh = DBI->connect("DBI:mysql:$db:$db_host:$db_port","$db_user","$db_pass")
		or die "connecting : $DBI::errstr\n";
	
	my $sth = $dbh->prepare ("SELECT do_not_scan FROM machines WHERE ip_address = \'$ip\'");
	
	$sth->execute();
	
	my $num_of_rows = $sth->rows;
	
	my @r;
	my $size_of_row;
	my $scan = 0;
	if($num_of_rows > 0) {
		while (@r = $sth->fetchrow_array) {
			$size_of_row = @r;
			if($size_of_row > 0) {
				$scan = $r[0];
			}
		}
		return $scan;
	} else {
		return 1;
	}
	
	$dbh->disconnect();
}

sub open_pid_file {
	my $file = shift(@_);
	if( -e $file ) {
		my $fh = IO::File->new($file) || return;
		my $pid = <$fh>;
		die "Server already running with PID $pid" if kill 0 => $pid;
		warn "Removing PID file for defunct server process $pid.\n";
		die "Can't unlink PID file $file" unless -w $file && unlink $file;
	}
	return IO::File->new($file,O_WRONLY|O_CREAT|O_EXCL,0644) or die "Can't create $file: $!\n";
}

sub become_daemon {
	die "Can't fork" unless defined (my $child = fork);
	exit 0 if $child;
	setsid();
	#print "\nStarting ExMon Daemon\n";
	open(STDIN, "/dev/null");
	open(STDOUT, "/dev/null");
	open(STDERR, "/dev/null");
	chdir '/';
	umask(0);
	$ENV{PATH} = '/bin:/sbin:/usr/bin:/usr/sbin';
	return $$;
}

sub ping_ip {
	my $host_ip = shift(@_);
	$ip_address = $host_ip;
	
	$mac_address = "n/a";
	$operating_system = "n/a";
	$hardware = "n/a";
	$host_name = "n/a";
	$user = "n/a";
	$uptime = "n/a";
	
	print("\n\n<------ ping $ip_address ------>\n");
	
	my $p = Net::Ping->new("icmp") or die "Can't create new ping object: $!\n";

	if ($p->ping($host_ip)) {
		print "$host_ip is alive\n";
		my $host_is_in_db = check_db_for_ip($host_ip);

		my $mac_address = get_mac_from_arp($host_ip);
		
		if($host_is_in_db != 0) {
			#update_existing_host();
		} else {
			add_new_host({
				ip_address			=> $ip_address,
				mac_address			=> "n/a",
				operating_system	=> "n/a",
				hardware			=> "n/a",
				host				=> "n/a",
				user				=> "n/a",
				uptime				=> "n/a",
			});
		}
	}
	else {
		print "$host_ip could not be contacted\n";
	}

	$p->close;
}

sub snmp_ip {
	my $host_ip = shift(@_);
	$ip_address = $host_ip;
	
	$mac_address = "n/a";
	$operating_system = "n/a";
	$hardware = "n/a";
	$host_name = "n/a";
	$user = "n/a";
	$uptime = "n/a";
	
	print("\n<------ snmp $ip_address ------>\n");
	
	my ($session, $error) = Net::SNMP->session(
		-timeout   => 1.5,
   		-hostname  => shift || $host_ip,
   		-community => shift || 'public',
   		-port      => shift || 161 
	);

	if (!defined($session)) {
   		printf("SESSION ERROR: %s.\n", $error);
		print "\nno snmp session\n";
   	} else {
		#### <! start - get the information > ####

		## mac address
		$mac_address = get_mac_from_arp($host_ip);

		## uptime
		my $uptimeResult = $session->get_request(
	   		-varbindlist => [$sysUpTimeOID]
		);
		if (!defined($uptimeResult)) {
		   printf("UPTIME ERROR: %s.\n", $session->error);
		} else {
			$uptime = $uptimeResult->{$sysUpTimeOID};
		}

		## operating system (splitting result if Windows into seperate hardware)

		my $operatingSystemResult = $session->get_request (
			-varbindlist => [$operatingSystemOID]
		);
		if (!defined($operatingSystemResult)) {
		   printf("OPERATING SYSTEM ERROR: %s.\n", $session->error);
		} else {
			my $temp1 = $operatingSystemResult->{$operatingSystemOID};

			if($temp1 =~ m/Windows/) {
				($hardware, $operating_system) = split(/ - /, $temp1);

				my ($hardwareTrash, $hardwareKeep) = split(/:/, $hardware);
				$hardwareKeep =~ s/^\s+//;
				$hardwareKeep =~ s/\s+$//;
				$hardware = $hardwareKeep;

				my ($osTrash, $osKeep) = split(/:/, $operating_system);
				$operating_system =~ s/^\s+//;
				$operating_system =~ s/\s+$//;
				$operating_system = $osKeep;
			} else {
				$operating_system = $temp1;
			}

		}

		## host name
		my $hostResult = $session->get_request (
			-varbindlist => [$hostOID]
		);
		if (!defined($hostResult)) {
		   printf("HOST NAME ERROR: %s.\n", $session->error);
		} else {
			$host_name = $hostResult->{$hostOID};
		}

		## installed applications
		my %installedHash;
		my $installedApplicationsResult = $session->get_table (
			-baseOID => $installedApplicationsOID
		);
		if (!defined($installedApplicationsResult)) {
			printf("INSTALLED APPLICATIONS ERROR: %s.\n", $session->error);
		} else { 
			%installedHash = %{$installedApplicationsResult};
			my $count = keys %installedHash;
			print "\nfound " . $count . " installed applications\n";
			my $temp;
			foreach $temp (keys %installedHash) {
				push(@installedApplications, $installedHash{$temp});
			}
		}
		%{$installedApplicationsResult} = ();
		%installedHash = ();

		$session->close;

		#### <! end - get the information > ####

		## check all variables ##
		if(defined($host_name)) {
			print("\nhost: " . $host_name);
		} else {
			$host_name = "n/a";
		}
		if(defined($operating_system)) {
			print("\noperating system: " . $operating_system);
		} else {
			$operating_system = "n/a";
		}
		if(defined($hardware)) {
			print("\nhardware: " . $hardware);
		} else {
			$hardware = "n/a";
		}
		if(defined($ip_address)) {
			print("\nip address: " . $ip_address);
		} else {
			$ip_address = "n/a";
		}
		if(defined($mac_address)) {
			print("\nmac address: " . $mac_address);
		} else {
			$mac_address = "n/a";
		}
		if(defined($user)) {
			print("\nuser: " . $user);
		} else {
			$user = "n/a";
		}
		if(defined($uptime)) {
			print("\nuptime: " . $uptime);
		} else {
			$uptime = "n/a";
		}

		print("\n\n");

		## test to see if machine is already in the database ##
		my $host_is_in_db = check_db_for_ip($host_ip);

		if($host_is_in_db != 0) {
			update_existing_host({
				id					=> $host_is_in_db,
				ip_address			=> $ip_address,
				mac_address			=> $mac_address,
				operating_system	=> $operating_system,
				hardware			=> $hardware,
				host				=> $host_name,
				user				=> $user,
				uptime				=> $uptime,
			});

			if(@installedApplications > 0) {
				update_installed_applications({
					machine_id				=> $host_is_in_db,
				}, @installedApplications);
			}
		} else {
			## took $mac_address ne "n/a" out due to it never being n/a anymore
			if($operating_system ne "n/a" || $hardware ne "n/a" || $host_name ne "n/a" || $user ne "n/a" || $uptime ne "n/a" || @installedApplications > 0) {
				add_new_host({
					ip_address			=> $ip_address,
					mac_address			=> $mac_address,
					operating_system	=> $operating_system,
					hardware			=> $hardware,
					host				=> $host_name,
					user				=> $user,
					uptime				=> $uptime,
				});

				my $check_for_host_again = check_db_for_ip($host_ip);
				if($check_for_host_again != 0) {
					if(@installedApplications > 0) {
						update_installed_applications({
							machine_id				=> $check_for_host_again,
						}, @installedApplications);
					}
				} ## else something didn't do what it was supposed to
			} else {
				print("\nno snmp info found.");
			}
		}
		#### clear values for next time around ####
		@installedApplications = ();
	}
	
}

sub nmap_ip {
	my $host_ip = shift(@_);
	$ip_address = $host_ip;
	
	print("\n<------ nmap $ip_address ------>\n");
	
	## setup and run nmap scan and save to XML file
	system("$nmap_path -v -v -v -sS -p 8080,1-1024 --host-timeout 5000 -O --max_rtt_timeout 50 -oX /Users/woodson/Desktop/test.xml $host_ip");

	## fill results from the XML file we just created
	my $scanner = new Nmap::Scanner;

	my $results = $scanner->scan_from_file('/Users/woodson/Desktop/test.xml');

	my $num_of_results = $results->nmap_run()->run_stats()->hosts()->up();

	if($num_of_results > 0) {
		## variable to store the guessed os
		my $nmap_os = "n/a";
		my $hostname = "n/a";
		
		my $hosts = $results->get_host_list();
		
		## check to see if already exists in the database (returns id if found, 0 if not)
		my $host_is_in_db = check_db_for_ip($host_ip);

		while(my $host = $hosts->get_next()) {
			#print "\nhost: " . $host->hostname();
			$hostname = $host->hostname();
			if($hostname ne "") {
				print "\nhost: " . $hostname;
			} else {
				print "\nhost: nmap didn't return value";
			}
			
			## get the list of guessed os matches, sort by accuracy, and store the most accuract in $nmap_os
			if(defined($host->os())) {
				my $os_match;
				my @sorted_accuracy;
				print "\nnmap thinks the os could be:";
				foreach $os_match ($host->os()->osmatches()) {
					push(@sorted_accuracy, {accuracy => $os_match->accuracy, os => $os_match->name()});
				}
				
				## sort list of possible os matches by accuracy number, then pick the first match
				my $size_of_sorted_accuracy = @sorted_accuracy;
				if($size_of_sorted_accuracy > 0) {
					sort{$b <=> $a} (@sorted_accuracy);
					$nmap_os = $sorted_accuracy[0]->{os};
				} else {
					$nmap_os = "n/a";
				}
				print "\nnmap chose os: " . $nmap_os;
			}

			my $ports = $host->get_port_list();
			
			## enter information to the correct place
			if($host_is_in_db != 0) {
				while(my $port = $ports->get_next()) {
					my $service_name;
					if(defined($port->service())) {
						$service_name = $port->service()->name();
					} else {
						$service_name = "unknown";
					}
					
					update_port({
						machine_id		=> $host_is_in_db,
						port_number		=> $port->portid(),
						service			=> $service_name,
						state			=> $port->state(),
					});
				}
				print "\n";
				
				## check db for values that nmap might have picked up
				if($hostname ne "n/a") {
					check_db_entry({
						machine_id		=> $host_is_in_db,
						entry_name		=> "host_name",
						entry_value		=> $hostname,
					})
				}
				if($nmap_os ne "n/a") {
					check_db_entry({
						machine_id		=> $host_is_in_db,
						entry_name		=> "operating_system",
						entry_value		=> $nmap_os,
					})
				}
			} else {
				while(my $port = $ports->get_next()) {
					#print "\nadd port:\t" . $port->portid() . " -\t" . $port->service()->name() . " -\t" . $port->state();
					add_new_port({
						machine_id		=> $host_is_in_db,
						port_number		=> $port->portid(),
						service			=> $port->service()->name(),
						state			=> $port->state(),
					});
				}
				print "\n";
				
				## check db for values that nmap might have picked up
				if($hostname ne "n/a") {
					check_db_entry({
						machine_id		=> $host_is_in_db,
						entry_name		=> "host_name",
						entry_value		=> $hostname,
					})
				}
				if($nmap_os ne "n/a") {
					check_db_entry({
						machine_id		=> $host_is_in_db,
						entry_name		=> "operating_system",
						entry_value		=> $nmap_os,
					})
				}
			}
		}
	}
	
	if(unlink('/Users/woodson/Desktop/test.xml') != 1) {
		print "\ncould not remove temporary xml file: $!\n";
	};
}

sub check_db_for_ip {
	my $host_to_check_for = shift(@_);
	
	my $dbh = DBI->connect("DBI:mysql:$db:$db_host:$db_port","$db_user","$db_pass")
		or die "connecting : $DBI::errstr\n";
	
	my $sth = $dbh->prepare ("SELECT * FROM machines WHERE ip_address = \'$host_to_check_for\'");
	
	$sth->execute();
	
	my $num_of_rows = $sth->rows;
	
	my @r;
	my $size_of_row;
	my $machine_id;
	if($num_of_rows > 0) {
		while (@r = $sth->fetchrow_array) {
			$size_of_row = @r;
			if($size_of_row > 0) {
				$machine_id = $r[0];
			}
		}
		return $machine_id;
	} else {
		return 0;
	}
	
	$dbh->disconnect();
}

## checks the arp cache for mac addresses
sub get_mac_from_arp {
	my $host_to_get_mac = shift(@_);
	
	my @arp_array = `arp -a`;

	my $size_of_arp_array = @arp_array;

	my $i;
	my @temp_array;
	my $temp_mac;
	for($i = 0; $i < $size_of_arp_array; $i++)
	{
		@temp_array = split(" ", $arp_array[$i]);
		if($arp_array[$i] =~ m/$host_to_get_mac/i)
		{
			if($arp_array[$i] !~ m/incomplete/i)
			{
				$temp_mac = $temp_array[3];
				return($temp_mac);
			}
		}
	}
	
	return("arp check failed");
}

#
# check_db_entry
# arguments:
#   machine_id
#   entry_name
#   entry_value
#
# description:
#   checks the "entry_name" in the db and if the db is n/a or nothing (but not undef)
#   then it updates the database with "entry_value"
#
sub check_db_entry {
	my ($args) = @_;
	
	print "\nchecking for $args->{entry_name}\n";
	
	## entry_value shouldn't be blank (since we're passing it), but just to double check
	if($args->{entry_value} eq "") {
		$args->{entry_value} = "n/a";
	}
	
	print "$args->{entry_name}: $args->{entry_value}\n";
	
	my $dbh = DBI->connect("DBI:mysql:$db:$db_host:$db_port","$db_user","$db_pass")
		or die "connecting : $DBI::errstr\n";
		
	my $sth = $dbh->prepare ("SELECT * FROM machines WHERE id  = \'$args->{machine_id}\'");
	
	$sth->execute();
	
	my $num_of_rows = $sth->rows;
	
	my $hash_ref;
	
	if($num_of_rows > 0) {
		while($hash_ref = $sth->fetchrow_hashref('NAME_lc')) {
			my $entry_in_db = $hash_ref->{$args->{entry_name}};
			if($entry_in_db eq "n/a" || $entry_in_db eq "") {
				my $sth2 = $dbh->prepare ("UPDATE machines SET $args->{entry_name} = \"$args->{entry_value}\" WHERE id = \"$args->{machine_id}\"");
				$sth2->execute();
			}
		}
	}
}

sub add_new_host {
	my ($args) = @_;
	
	my $dbh = DBI->connect("DBI:mysql:$db:$db_host:$db_port","$db_user","$db_pass")
		or die "connecting : $DBI::errstr\n";
	
	my $sth = $dbh->prepare ("INSERT INTO machines (ip_address, mac_address, operating_system, hardware, host_name, user, uptime, date_added, last_time_checked) VALUES (\"$args->{ip_address}\", \"$args->{mac_address}\", \"$args->{operating_system}\", \"$args->{hardware}\", \"$args->{host}\", \"$args->{user}\", \"$args->{uptime}\", NOW(), NOW())");
	
	$sth->execute();
	
	$dbh->disconnect();
}

sub update_existing_host {
	my ($args) = @_;
	
	my $dbh = DBI->connect("DBI:mysql:$db:$db_host:$db_port","$db_user","$db_pass")
		or die "connecting : $DBI::errstr\n";
	
	my $sth = $dbh->prepare ("UPDATE machines SET mac_address = \"$args->{mac_address}\", operating_system = \"$args->{operating_system}\", hardware = \"$args->{hardware}\", host_name = \"$args->{host}\", user = \"$args->{user}\", uptime = \"$args->{uptime}\", last_time_checked = NOW() WHERE id = \"$args->{id}\"");
	
	$sth->execute();
	
	$dbh->disconnect();
	
}

sub add_new_port {
	my ($args) = @_;
	
	print "\nadd new port";
	
	my $dbh = DBI->connect("DBI:mysql:$db:$db_host:$db_port","$db_user","$db_pass")
		or die "connecting : $DBI::errstr\n";
	
	my $sth = $dbh->prepare ("INSERT INTO port_entries (machine_id, port_number, service, state, last_time_found) VALUES (\"$args->{machine_id}\", \"$args->{port_number}\", \"$args->{service}\", \"$args->{state}\", NOW())");
	
	$sth->execute();
	
	$dbh->disconnect();
}

sub update_port {
	my ($args) = @_;
	
	print "\nupdate port";
	
	my $dbh = DBI->connect("DBI:mysql:$db:$db_host:$db_port","$db_user","$db_pass")
		or die "connecting : $DBI::errstr\n";
		
	my $sth = $dbh->prepare ("SELECT * FROM port_entries WHERE machine_id  = \'$args->{machine_id}\' AND port_number = \'$args->{port_number}\'");
	
	$sth->execute();
	
	my $num_of_rows = $sth->rows;
	
	my @r;
	
	if($num_of_rows > 0) {
		while (@r = $sth->fetchrow_array) {
			my $port_id = $r[0];
			my $sth2 = $dbh->prepare ("UPDATE port_entries SET state = \"$args->{state}\", last_time_found = NOW() WHERE id = \"$port_id\"");
			$sth2->execute();
		}
	} else {
		print "\n" . $args->{machine_id};
		print "\t" . $args->{port_number};
		print "\t" . $args->{service};
		print "\t" . $args->{state};
		
		add_new_port({
			machine_id		=> $args->{machine_id},
			port_number		=> $args->{port_number},
			service			=> $args->{service},
			state			=> $args->{state},
		});
	}
}

sub update_installed_applications {
	my ($args, @array) = @_;
	
	print "\nupdate installed applications";
	
	## for now we're just going to delete everything and re-add them all
	my $dbh = DBI->connect("DBI:mysql:$db:$db_host:$db_port","$db_user","$db_pass")
		or die "connecting : $DBI::errstr\n";
		
	my $sth = $dbh->prepare ("DELETE FROM installed_applications WHERE machine_id  = \'$args->{machine_id}\'");
	
	$sth->execute();
	
	
	my $name;
	foreach $name (@array) {
		if ($name =~ m/\w'\w/i) {
			$name =~ s/'/\\'/
		}
		my $sth2 = $dbh->prepare ("INSERT INTO installed_applications (machine_id, name) VALUES (\'$args->{machine_id}\', \'$name\')");
		$sth2->execute();
	}
}

END { unlink PID_FILE if $pid == $$; }
